#include <Arduino.h>
#include <Adafruit_I2CDevice.h>
#include <WiFi.h>
#include <Adafruit_GFX.h>      
#include <Adafruit_ST7735.h>
#include <MFRC522v2.h>
#include <MFRC522DriverSPI.h>
#include <MFRC522DriverPinSimple.h>
#include <MFRC522Debug.h>
#include <Preferences.h>
#include <BLEAdvertisedDevice.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include "time.h"
#include <unordered_map>

using namespace std;

// 在Adafruit_SPITFT::initSPI()里会拉高DC管脚 pinMode(_dc, OUTPUT);
// digitalWrite(_dc, HIGH); // Data mode
// GPIOs for TFT's SPI, other default pins, please see 
// platformio\packages\framework-arduinoespressif32\variants\AirM2M_CORE_ESP32C3\pins_arduino.h
// GPIO_19用于TFT的片选管脚，缺省的GPIO_7用做MFRC522的片选管脚。
#define TFT_DC   6
#define TFT_CS   19

// MFRC522 chip select pin (GPIO_7)
#define MFRC_CS   7

// GPIOs for LED, hardwired in AirM2M board
// 合宙CORE-ESP32开发板 GPIO_12接D4, GPIO_13接D5
#define LED_D4  12
#define LED_D5  13

// pin to 1 Relay Module, High level trigger
// GPIO_4 接1路5V继电器的IN端，继电器设置为高电平触发，接通5V钛丝电控锁
#define LOCK_PIN  4

// 五向开关用到上/下/中键，用于在录入信息时向上/下选择同学，中键确认/保存选择。
#define LCD_BUTTON_UP         0        // GPIO_0
#define LCD_BUTTON_CENTER     1        // GPIO_1
#define LCD_BUTTON_DOWN       18       // GPIO_18
#define buttonUp              1        // 按钮返回值定义
#define buttonDown            2        //
#define buttonCenter          5        //

// WIFI信息用于启动时通过NTP协议同步系统时间
const char* ssid     = "your-ssid";
const char* password = "your-password";

// 阿里云服务器，校园网内可能要换成其他NTP服务器
#define NTP1 "ntp1.aliyun.com"
#define NTP2 "ntp2.aliyun.com"
#define NTP3 "ntp3.aliyun.com"

// 班级同学名单
string stdt_name[] = {"cyz","ck","csq","cxl","cyn",
                      "jyy","lny","qsl","sxy","",
                      "wxy","xl","zty","zcy","zyc",
                      "bsh","czh","chr","cjl","cjl",
                      "cyj","cyz","fj","gek","gqh",
                      "hzk","jyz","sjz","smq","tcl",
                      "tsy","xyh","xzy","ycm","ytx",
                      "zll","cyr","zyy","yxc"};

unsigned int numStdts = sizeof(stdt_name)/sizeof(stdt_name[0]);

const int CUTOFF = -50;   //蓝牙信号RSSI阈值，低于此认为远离手机柜
const char binaryIcCard[] = {0xC5, 0x27, 0x79, 0x39, 0x00};   //手机管家IC卡数据
string adminIcCard = binaryIcCard;

string missingList = "";

const unsigned int classBegin = 8;    //只在8:00-17:00时间段内扫描手机是否在柜内
const unsigned int classOver = 17;
bool alwaysReportFirstTime = true;    //系统启动的第一次总是检查手机是否全部在柜内

// LCD屏初始化，跟MFRC522复用默认SPI数据接口，TFT_CS作为LCD的片选脚。
Adafruit_ST7735 tft(TFT_CS, TFT_DC, -1);

// MFRC522初始化，使用默认的SPI数据接口，MFRC_CS(GPIO_7)做为MFRC522的片选脚。
MFRC522DriverPinSimple ss_pin(MFRC_CS);
MFRC522DriverSPI driver{ss_pin}; // Create SPI driver.
MFRC522 mfrc522{driver}; // Create MFRC522 instance.创建MFRC522实例

// 存放系统时间信息，在启动时NTP成功同步到网络时间后，调用
// getLocalTime()，从本地时钟读取时间。
struct tm timeinfo;

// 使用Preferences库API来实现flash存储功能
Preferences prefs; // 声明Preferences对象
//Flash
// "GuardianPhoneU"  当前程序所使用的命名空间
// "stdtBitMasks" 64位长整型数，从bit0起每一位代表班级的一名同学, bit0代表cyz
uint64_t stdtBitMasks = 0; //例如0b00000…00011110010001100010000001
// "cyz", "ck",... 每位同学保存在flash的信息，都是以同学名称为key保存的saved_info结构体 
struct saved_info {
  uint8_t stdt_id;                  //学生编号 0对应实际学号1
  char    name[15];                 //姓名 见stdt_name[]
  char    ic_uid[10];               //校园卡id, RC522读取到的信息
  char    bleName[40];              //手机名称，蓝牙读取到的手机名称
  char    bleManufacturerData[100]; //蓝牙Data，蓝牙协议里面的厂家数据段
};

// 内存中对应的同学信息结构体
struct student_info {
  uint8_t stdt_id;             //学生编号
  string  name;                //姓名
  string  ic_uid;              //校园卡id
  string  bleName;             //手机名称
  string  bleManufacturerData; //蓝牙Data
  int     retryTimes = 3;      // 允许报警前重试扫描次数
  bool    currentStat = false; // 当前扫描手机是否在附近
};
// 为了操作简单，内存中预分配了全部同学的信息作为静态数组
student_info studentInfos[sizeof(stdt_name)/sizeof(stdt_name[0])];

// 校园卡-学生编号Map表
unordered_map<string, uint8_t> icUids;
// 蓝牙数据段-学生编号Map表
unordered_map<string, uint8_t> bleData;

// 串口初始化，波特率是9600
void initSerial() {
  Serial.begin(9600);
}

// 液晶屏初始化
void initTFT() {
  tft.initR(INITR_MINI160x80); //屏幕分辨率 80x160
  tft.setRotation(3);
  tft.fillScreen(ST7735_BLACK); //LCD清空
  tft.setTextSize(2); //设置字体大小
  tft.setCursor(0, 0);//从左上角原点显示
  Serial.print("TFT OK");
}

// 清屏
void clearTFT() {
  tft.fillScreen(ST7735_BLACK);
  tft.setTextSize(2);
  tft.setCursor(0, 0);
}

// 初始化LED灯
void initLED() {
  pinMode(LED_D4, OUTPUT);
  pinMode(LED_D5, OUTPUT);
  digitalWrite(LED_D4, LOW); // 缺省灭灯
  digitalWrite(LED_D5, LOW); 
}

// 初始化锁，LOCK_PIN连接到继电器IN端口，高电平连通电控锁开锁
void initLock() {
  pinMode(LOCK_PIN, OUTPUT);
  digitalWrite(LOCK_PIN, LOW); // Default is lock the door
}

// 点亮LED灯
void ledOn() {
  digitalWrite(LED_D4, HIGH);
  digitalWrite(LED_D5, HIGH);
}

// 熄灭LED灯
void ledOff() {
  digitalWrite(LED_D4, LOW);
  digitalWrite(LED_D5, LOW);
}

// 快速闪灯
void blinkLed() {
  int d4Stat = digitalRead(LED_D4);
  int d5Stat = digitalRead(LED_D5);
  int blink = 50;
  for(int i = 0; i < 6; i++) {
    ledOn();
    delay(blink);
    ledOff();
    delay(blink);
  }
  digitalWrite(LED_D4, d4Stat);
  digitalWrite(LED_D5, d5Stat);
}

// 成功同步时间
bool printLocalTime(){
  //struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    delay(500);
    clearTFT();
    tft.println("Failed to obtain time");
    return false;
  }
  return true;
}

// NTP同步时间，只在启动时运行，同步完成后关闭WIFI
void syncTime() {
  tft.print("Connecting to ");
  tft.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    tft.print(".");
  }
  tft.println("");
  tft.println("WiFi connected.");
  
  // Init and get the time
  // 东八时区
  configTime(8 * 3600, 0, NTP1, NTP2, NTP3);

  while(!printLocalTime());

  // disconnect WiFi as it's no longer needed
  // rtc clock is only synced during startup
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

// 初始化Flash
void initFlash(){
  prefs.begin("GuardianPhoneU"); // 打开命名空间GuardianPhoneU
  prefs.end();
}

// 初始化MFRC522模块
void initMFRC522(){
  mfrc522.PCD_Init(); // Init MFRC522 board.
}

// 初始化蓝牙BLE
void initBluetooth() {
  BLEDevice::init("AirM2M");
}

// 设置按键的GPIO
void setup_buttons(){
  pinMode(LCD_BUTTON_DOWN,INPUT_PULLUP);
  pinMode(LCD_BUTTON_CENTER,INPUT_PULLUP);
  pinMode(LCD_BUTTON_UP,INPUT_PULLUP);  
}

// 将一个记录保存在flash里
void saveOneRecord(student_info *info) {
  saved_info oneRecord;

  oneRecord.stdt_id = info->stdt_id;
  strncpy(oneRecord.name, info->name.c_str(), sizeof(oneRecord.name));
  strncpy(oneRecord.ic_uid, info->ic_uid.c_str(), sizeof(oneRecord.ic_uid));
  strncpy(oneRecord.bleName, info->bleName.c_str(), sizeof(oneRecord.bleName));
  strncpy(oneRecord.bleManufacturerData, info->bleManufacturerData.c_str(), sizeof(oneRecord.bleManufacturerData));

  prefs.begin("GuardianPhoneU");
  prefs.putBytes(info->name.c_str(), (const void*)&oneRecord, sizeof(saved_info));
  prefs.end();
}

// 从Flash里读取一个记录
student_info loadOneRecord(string *name) {
  student_info info;
  saved_info oneRecord;

  // load info from flash to mem
  prefs.begin("GuardianPhoneU");
  prefs.getBytes(name->c_str(), &oneRecord, sizeof(saved_info));
  prefs.end();

  // convert from saved_info to student_info
  info.stdt_id = oneRecord.stdt_id;
  info.name = oneRecord.name;
  info.ic_uid = oneRecord.ic_uid;
  info.bleName = oneRecord.bleName;
  info.bleManufacturerData = oneRecord.bleManufacturerData;

  return info;
}

// 保存bitMasks到Flash
void saveBitMasks() {
  prefs.begin("GuardianPhoneU");
  prefs.putULong64("stdtBitMasks", stdtBitMasks);
  prefs.end();
}

// 从Flash读取bitMasks，如果读不到，stdtBitMasks设置为0
void loadBitMasks() {
  prefs.begin("GuardianPhoneU");
  stdtBitMasks = prefs.getULong64("stdtBitMasks", 0);
  prefs.end();
}

// 加载Flash里所有合法记录
void loadFlashRecords() {
  loadBitMasks();

  size_t stdtLimit = numStdts;
  student_info oneRcrd;
  string nameList;
  const uint64_t one = 1;

  for(int i = 0; i < stdtLimit; i++) {
    // only load valid information to memory
    if (stdtBitMasks & (one << i)) {
      oneRcrd = loadOneRecord(&stdt_name[i]);
      icUids[oneRcrd.ic_uid] = i;
      bleData[oneRcrd.bleManufacturerData] = i;
      studentInfos[i] = oneRcrd;
      nameList += stdt_name[i];
    };
  }

  tft.print("Records loaded:");
  tft.println(nameList.c_str());
  delay(5000);
}

// 是不是管理员卡
bool isAdminCard(string card) {
  if (card.compare(adminIcCard)) {
    return false;
  } else {
    return true;
  }
}

// 是不是已注册的校园卡
bool isAnExistingCard(string card) {
  auto search = icUids.find(card);
  if (search != icUids.end()) {
    return true;
  } else {
    return false;
  }
}

// 开锁
void openDoor() {
  digitalWrite(LOCK_PIN, HIGH);

  // according to the lock user manual, the current time 
  // is no more than 800ms. We use 500ms.
  // 根据厂商说明，触发时长不能超过800ms。
  delay(500);

  digitalWrite(LOCK_PIN, LOW);
}

// 是否是已注册的蓝牙信息
bool isAnExistingBleData(string ble) {
  auto search = bleData.find(ble);
  if(search != bleData.end())
    return true;
  else
    return false;
}

// 手机状态异常报警：亮灯并打印名单。LED灯只有管理员再次扫描IC卡后才会熄灭。
void upWarning(string mlist) {
  // light up led
  ledOn();

  //print missing list
  tft.println("Phone missing:");
  tft.println(mlist.c_str());
  delay(5000);
}

// 通过蓝牙BLE扫描码手机，低于CUTOFF认为不在手机柜内
void scanPhones() {
  clearTFT();
  tft.println("Phone scanning...");

  size_t stdtLimit = numStdts;
  const uint64_t one = 1;
  // 默认手机最新状态都为False
  for(int i = 0; i < stdtLimit; i++) {
    if (stdtBitMasks & (one << i)) {
      studentInfos[i].currentStat = false;
    };
  }

  BLEScan *scan = BLEDevice::getScan();
  scan->setActiveScan(true);
  BLEScanResults results = scan->start(5); //扫描5秒，扫描结果保存在results中
  for (int i = 0; i < results.getCount(); i++) { //循环遍历扫描结果
    BLEAdvertisedDevice device = results.getDevice(i); // 读取results中第i个设备
    int rssi = device.getRSSI(); // 获取当前设备的rssi值
    if (rssi > CUTOFF) {  // 如果rssi值高于蓝牙检测的阈值，认为这个设备在附近
      string data = device.getManufacturerData(); // 读取当前蓝牙设备的ManufacturerData字段
      if (isAnExistingBleData(data)) { // 在本地蓝牙数据段-学生编号表中快速查找是否是已注册设备
        int stdtNum = bleData[data]; // 反查出当前设备对应的学生编号
        studentInfos[stdtNum].currentStat = true; // 标记当前学生的手机状态为正常
      }
    }
  }

  // sum up the missing list 汇总失踪手机列表
  missingList = "";
  bool changed = false;
  // 只汇总stdtBitMasks已注册的同学信息
  for(int i = 0; i < stdtLimit; i++) {
    if (stdtBitMasks & (one << i)) {
      // Phone is missing.
      if(studentInfos[i].currentStat == false) {
          // add all missing phone's name into the missing list
          missingList += studentInfos[i].name + " ";
          if (studentInfos[i].retryTimes != 0) {
            studentInfos[i].retryTimes--;
            if (studentInfos[i].retryTimes == 0){
              changed = true; 
            }
          }

          // Call upWarning() only when stat true->false
          // 只有当手机状态从正常变为不正常的情况下，亮灯报警，这样可以减少手机管理员的压力
          // 但系统第一次开始扫描手机，有异常状态的话，均会报警，这对应着8:00上课时手机都应该放入柜内
          // 或者系统在课间时段重启时，手机也都应该在柜内
          if (alwaysReportFirstTime) {
            changed = true;
            alwaysReportFirstTime = false;
          }
      } else {
        studentInfos[i].retryTimes = 3;
      }
    }
  }
  alwaysReportFirstTime = false;

  if (changed) {
    upWarning(missingList);
  }
}

// 扫描按键
unsigned int scanButton() {
  int d,u,c;
  d=digitalRead(LCD_BUTTON_DOWN);
  u=digitalRead(LCD_BUTTON_UP);
  c=digitalRead(LCD_BUTTON_CENTER);

  if(d==LOW && c != LOW) {
    Serial.write("down!\n");
    return buttonDown;
  } else if(u==LOW && c!=LOW) {
      Serial.write("up!\n");
      return buttonUp;
  } else if(c==LOW && d!=LOW && u!=LOW) {
      Serial.write("center!\n");
      return buttonCenter;
  } else {
      return 0;
  }
}

void iAmManager() {
  clearTFT();
  tft.println(missingList.c_str());
  delay(10000);
  ledOff();  
  clearTFT();
  tft.println("Yes, m'Lord!");
  tft.println();
  tft.println("U have 3 wishes.");
  delay(5000);

  const int CD3000 = 3000;
  int countdown = CD3000;
  int stdtNumber = 0;
  bool timeout = true;
  bool icTimeout = true;
  student_info newguy;

  clearTFT();
  tft.println("Up/Down/ Center,");
  tft.println("Select/ Confirm");
  tft.println(stdt_name[stdtNumber].c_str());

  // Use up/down button to select student
  while (countdown) {
    unsigned int action = scanButton();
    switch(action) {
      case 0: {
        delay(10);
        countdown--;
        break;
      };
      case buttonUp: {
        countdown = CD3000;
        if(stdtNumber == 0) {
          stdtNumber = numStdts - 1;
        } else {
          stdtNumber--;
          if(stdtNumber == 9) {
            //bypass sxy
            stdtNumber--;
          }
        }
        clearTFT();
        tft.println(stdt_name[stdtNumber].c_str());
        delay(500);
        break;
      };
      case buttonDown: {
        countdown = CD3000;
        if(stdtNumber == numStdts - 1) {
          stdtNumber = 0;
        } else {
          stdtNumber++;
          if(stdtNumber == 9) {
            //bypass sxy
            stdtNumber++;
          }
        }
        clearTFT();
        tft.println(stdt_name[stdtNumber].c_str());
        delay(500);
        break;
      };
      case buttonCenter: {
        countdown = 0;
        timeout = false;
        delay(500);
        break;
      };
      default: {
        delay(10);
        countdown--;
        break;
      }
    }
  }
  if (timeout) {
    clearTFT();
    tft.println("Button timeout.");
    return;
  }

  clearTFT();
  tft.print("Scan ");
  tft.print(stdt_name[stdtNumber].c_str());
  tft.println("'s IC card:");
  tft.println("Center to remove info.");

  newguy.stdt_id = stdtNumber;
  newguy.name = stdt_name[stdtNumber];

  //scan icCard
  countdown = CD3000 / 6;
  timeout = true;
  while (countdown) {
    if (buttonCenter == scanButton()) {
      // remove info from map icUids
      auto uid = icUids.find(studentInfos[stdtNumber].ic_uid);
      if (uid != icUids.end())
        icUids.erase(uid);

      // remove info from map bleData
      auto ble = bleData.find(studentInfos[stdtNumber].bleManufacturerData);
      if(ble != bleData.end())
        bleData.erase(ble);

      // clear bitMasks
      const uint64_t one = 1;
      stdtBitMasks  = stdtBitMasks & ~(one << stdtNumber);
      saveBitMasks();

      clearTFT();
      tft.print(stdt_name[stdtNumber].c_str());
      tft.println("'s record is removed.");
      delay(5000);
      return;
    }
    if (!mfrc522.PICC_IsNewCardPresent()) {
      countdown--;
      continue;
    }
    if (!mfrc522.PICC_ReadCardSerial()) {
      countdown--;
      continue;
    }
    newguy.ic_uid = (char *)mfrc522.uid.uidByte;
    timeout = false;
    break;
  }

  if (timeout) {
    tft.println("IC Card timeout.");
    delay(2000);
    return;
  }

  clearTFT();
  tft.println(newguy.ic_uid.c_str());
  tft.println("");
  tft.println("Phone's BLE:");

  // scan Phones
  countdown = 30;
  timeout = true;
  BLEAdvertisedDevice bestDevice;
  while(countdown) {
    BLEScan *scan = BLEDevice::getScan();
    scan->setActiveScan(true);
    BLEScanResults results = scan->start(2);
    const int veryClose = -50;
    int best = veryClose;
    for (int i = 0; i < results.getCount(); i++) {
      BLEAdvertisedDevice device = results.getDevice(i);
      int rssi = device.getRSSI();
      if (rssi > best){
        best = rssi;
        bestDevice = device;
      }
    }
    if (best == veryClose){
      countdown--;
    } else {
      timeout = false;
      break;
    }
  }
  if (timeout) {
    clearTFT();
    tft.println("Phone timeout.");
    return;
  }
  newguy.bleName = bestDevice.getName();
  newguy.bleManufacturerData = bestDevice.getManufacturerData();

  clearTFT();
  tft.println(newguy.bleName.c_str());
  delay(5000);
  
  // confirm information
  countdown = CD3000;
  timeout = true;
  while (countdown) {
    unsigned int action = scanButton();
    switch(action) {
      case 0: {
        delay(10);
        countdown--;
        break;
      };
      case buttonCenter: {
        countdown = 0;
        timeout = false;
        break;
      };
      default: {
        delay(10);
        countdown--;
        break;
      }
    }
  }
  if (timeout) {
    clearTFT();
    tft.println("Final confirm timeout");
    return;
  }

  clearTFT();
  tft.print("Saving ");
  tft.print(stdt_name[stdtNumber].c_str());
  tft.println("'s record.");

  // update memory studentInfos
  studentInfos[stdtNumber] = newguy;

  // update icUids and bleData
  icUids[newguy.ic_uid] = stdtNumber;
  bleData[newguy.bleManufacturerData] = stdtNumber;

  // update bitMasks
  const uint64_t one = 1;
  stdtBitMasks  = stdtBitMasks | (one << stdtNumber);
  saveBitMasks();

  // save to flash
  saveOneRecord(&studentInfos[stdtNumber]);
  delay(5000);
}

void setup() {
    initLock();
    initLED();
    ledOn();
    initSerial();
    initTFT();
    syncTime();
    initFlash();
    initMFRC522();
    initBluetooth();
    setup_buttons();
    loadFlashRecords();
    ledOff();
}

unsigned int loopCountDown = 30;
void loop() {

  if (!getLocalTime(&timeinfo)) {
    clearTFT();
    tft.println("Failed to obtain time");
  }

  // Reset the loop if no new card present on the sensor/reader. This saves the entire process when idle.
  if (!mfrc522.PICC_IsNewCardPresent()) {
    clearTFT();
    tft.println(&timeinfo, "%H:%M:%S");
    delay(1000);
    loopCountDown--;
    // scan BLE phones every 30 secs
    if (loopCountDown == 0) {
      // only scan phone during 8:00 - 17:00
      //if (timeinfo.tm_hour >= classBegin && timeinfo.tm_hour < classOver) {
        scanPhones();
      //}
      loopCountDown = 30;
      
    }
    return;
  } 

  // Select one of the cards.
  if (!mfrc522.PICC_ReadCardSerial()) {
    return;
  }
  
  string newCard = (char *)mfrc522.uid.uidByte;

  if (isAdminCard(newCard)) {
    openDoor();
    iAmManager();
  } else {
    // Other IC cards
    if (isAnExistingCard(newCard)) {
      openDoor();
      clearTFT();
      tft.println("Put Phone in,");
      tft.println();
      tft.println("Lock the door.");
      delay(2000);
    } else {
      clearTFT();
      tft.print("Unknown Card:");
      tft.println(newCard.c_str());
      blinkLed();
      delay(2000);
    }
  }

}
